package com.fzk.utils;


import java.io.*;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.stream.Stream;
import java.util.zip.GZIPOutputStream;

/**
 * 这里主要是记录的目录的相关工具方法，对于单个文件的操作FileInputStream可能已经够了
 * 主要用到 Files 工具类和 Path 类
 *
 * @author fzk
 * @datetime 2021-11-23 23:40
 */
@SuppressWarnings("unused")
public class MyFileUtil {
    // suffix-->content-type
    public static final HashMap<String, String> imageSuffixMap;

    static {
        imageSuffixMap = new HashMap<>();
        // content-type fileSuffix
        String[] imageSuffix = new String[]{
                "image/gif  gif GIF",
                "image/ief  ief",
                "image/jpeg jpeg jpg jpe JPG",
                "image/tiff tiff tif",
                "image/png  png PNG",
                "image/x-xwindowdump    xwd",
                "image/x-icon   ico",
                "image/bmp  bmp",
                "image/webp webp"
        };
        for (String suffix : imageSuffix) {
            String[] splits = suffix.split("\\s+");
            for (int i = 1; i < splits.length; i++) {
                imageSuffixMap.put(splits[i], splits[0]);
            }
        }
    }

    /**
     * 复制文件目录
     *
     * @param sourceDir 源目录
     * @param targetDir 目标目录
     * @param isCovered 是否覆盖
     * @apiNote 不知道为什么，如果设为true，windows的文件不但手动难以删除，这里也会被一直卡主
     */
    public static void copyDir(Path sourceDir, Path targetDir, boolean isCovered) throws IOException {
        assert sourceDir != null && targetDir != null : "sourceDir or targetDir is null";

        // 1.判断是否是目录：不存在或存在而不是目录都返回false
        if (!Files.isDirectory(sourceDir))
            throw new IllegalArgumentException("sourceDir is not a directory or it not exists");

        if (Files.exists(targetDir) && !Files.isDirectory(targetDir))
            throw new IllegalArgumentException("targetDir exists but not a directory");
        // 这里可以创建到targetSource的父级目录
        if (Files.notExists(targetDir.getParent()))
            Files.createDirectories(targetDir.getParent());

        // 2.前面没有问题就正式开始copy了
        try (Stream<Path> stream = Files.walk(sourceDir)) {
            stream.forEach(p -> {
                try {
                    // 找到相对于target的绝对文件path，resolve方法在解析path时，如果是绝对路径则直接返回，否则转为相对于的绝对路径
                    Path q = targetDir.resolve(sourceDir.relativize(p));
                    if (Files.isDirectory(p)) {
                        try {
                            Files.createDirectory(q);// 这个方法会在目录已经存在的情况下会报错
                        } catch (FileAlreadyExistsException e) {
                            if (!isCovered) throw e;// 指定覆盖则忽略
                        }
                    } else {
                        if (isCovered) // 指定覆盖行为
                            Files.copy(p, q, StandardCopyOption.REPLACE_EXISTING);
                        else Files.copy(p, q);
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        }
    }

    /**
     * 删除一个目录，必须在先删除其子文件和子目录，否则DirectoryNotEmptyException
     *
     * @param dirPath 目录
     * @throws IOException 可能的IO异常
     */
    public static void deleteDir(Path dirPath) throws IOException {
        // 1.先检查path是否合法
        if (Files.notExists(dirPath))
            return;
        if (!Files.isDirectory(dirPath))
            throw new NotDirectoryException("dirPath not a directory");

        // 这个方法遍历文件树，在目录不存在的时候不会抛出异常
        Files.walkFileTree(dirPath, new SimpleFileVisitor<>() {
            /**
             * 在一个目录被处理前调用
             * @param dir 目录引用
             * @param attrs 目录基本属性
             * @return 访问结果
             */
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                Objects.requireNonNull(dir);
                Objects.requireNonNull(attrs);

//                System.out.println("pre------: "+dir.toString());
                return FileVisitResult.CONTINUE;
            }

            /**
             * 遇到目录里的 文件 时的调用
             * @param file 对文件的引用
             * @param attrs 文件的基本属性
             * @return 访问结果
             * @throws IOException 如果发生 I/O 错误
             */
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Objects.requireNonNull(file);
                Objects.requireNonNull(attrs);

                // 删除文件
//                System.out.println("file------: "+file.toString());
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            /**
             * 试图访问文件或目录发生错误时，如 没有权限
             * @param file 对文件的引用
             * @param exc 阻止访问文件的 I/O 异常
             * @return 访问结果
             */
            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) {
                Objects.requireNonNull(file);
//                throw exc;
                return FileVisitResult.SKIP_SIBLINGS;// 遇到异常，采取跳过策略
            }

            /**
             * 在目录中的条目及其所有后代都被访问后调用 <br/>
             * 目录的迭代过早完成时（通过返回SKIP_SIBLINGS的visitFile方法，或迭代目录时的 I/O 错误），也会调用此方法
             * @param dir 目录引用
             * @param exc 如果目录的迭代完成且没有错误，则为null ； 否则导致目录迭代过早完成的I/O异常
             * @return 访问结果
             * @throws IOException 如果发生IO异常
             */
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Objects.requireNonNull(dir);
                if (exc != null)
                    throw exc;

                // 把目录给删了
//                System.out.println("post------: "+dir.toString());
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * gzip压缩，对于文本压缩效率高，对于视频图片则效果不明显
     * 比如可以将16MB日志文件压缩到72KB
     *
     * @param path 待压缩文件
     * @throws IOException 如文件不存在
     */
    public static void gzip(Path path) throws IOException {
        try (FileInputStream in = new FileInputStream(path.toFile());
             GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(path + ".gz"))) {
            byte[] buf = new byte[4 * 1024];// 4k读取
            int len;
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
            out.flush();
        }
    }

    /**
     * 确保文件的目录和文件都已创建
     *
     * @param file 文件
     */
    public static void ensureDirsAndFile(Path file) throws IOException {
        if (Files.notExists(file)) {
            Path dir = file.getParent();
            if (Files.notExists(dir)) {
                Files.createDirectories(dir);
            }
            Files.createFile(file);
        }
    }

    /**
     * 确保目录已经创建
     *
     * @param dir 目录路径
     */
    public static void ensureDirs(Path dir) throws IOException {
        if (Files.notExists(dir)) {
            Files.createDirectories(dir);
        }
    }

    /**
     * 加载指定包下的所有class文件
     *
     * @param packageName 包名, 如com.fzk.controller
     * @return class文件, 没有则为空集合, 不会为null
     */
    public static List<Class<?>> loadPackageClasses(String packageName) throws IOException, ClassNotFoundException {
        ArrayList<Class<?>> classes = new ArrayList<>();
        ArrayList<String> classNames = new ArrayList<>();// 全类名

        final URL packageDirUrl = Thread.currentThread().getContextClassLoader().getResource(packageName.replaceAll("\\.", "/"));
        if (packageDirUrl == null) {
            return classes;
        }
        // 1.file协议, 一般是IDEA直接运行
        if (packageDirUrl.getProtocol().equals("file")) {
            final Path packageDirPath = Path.of(new File(packageDirUrl.getFile()).getPath());
            if (!Files.isDirectory(packageDirPath)) {
                throw new RuntimeException("");
            }
            // 遍历文件协议目录下所有文件
            Queue<Path> dirs = new LinkedList<>();
            dirs.add(packageDirPath);
            while (dirs.size() > 0) {
                Path curDir = dirs.poll();
                try (DirectoryStream<Path> stream = Files.newDirectoryStream(curDir)) {
                    stream.forEach((cur) -> {
                        if (Files.isDirectory(cur)) {
                            dirs.add(cur);
                        } else if (isClassFile(cur)) {// 只添加class文件
                            classNames.add(getClassname(cur, packageName));
                        }
                    });
                }
            }
        } else if (packageDirUrl.getProtocol().equals("jar")) {
            // 2.jar协议，一般是运行可执行jar
            JarURLConnection urlConnection = (JarURLConnection) packageDirUrl.openConnection();
            Enumeration<JarEntry> entries = urlConnection.getJarFile().entries();
            while (entries.hasMoreElements()) {
                // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
                JarEntry entry = entries.nextElement();
                if (!entry.isDirectory()) {
                    String name = entry.getName();
                    if (name.endsWith(".class")) {
                        // 注意这个在windows下也是 com/fzk/Hello.class
                        name = name.replaceAll("/", ".");
                        name = name.replaceAll("\\\\", ".");
                        // 全类名 com.fzk.Hello
                        String classname = name.substring(0, name.lastIndexOf(".class"));
                        classNames.add(classname);
                    }
                }
            }
        }

        // 3.加载class文件，会引起class的静态初始化
        for (String classname : classNames) {
            Class<?> clazz = Class.forName(classname);
            classes.add(clazz);
        }
        return classes;
    }

    /**
     * class文件绝对路径获取全类名
     *
     * @param classPath   class文件绝对路径, 如 D:\\test\\com\\fzk\\Hello.class 或 /opt/com/fzk/Hello.class
     * @param packageName 包名, 如com.fzk
     * @return 全类名, 如com.fzk.Hello
     */
    private static String getClassname(Path classPath, String packageName) {
        String absolutePath = classPath.toAbsolutePath().toString();// D:\\test\\com\\fzk\\Hello.class 或 /opt/com/fzk/Hello.class
        if (System.getProperty("os.name").toLowerCase().contains("windows")) {
            // D:\\test\\com\\fzk\\Hello.class 替换为 D:.test.com.fzk.Hello.class
            absolutePath = absolutePath.replaceAll("\\\\", ".");
        } else {
            // /opt/com/fzk/Hello.class 替换为 .opt.com.fzk.Hello.class
            absolutePath = absolutePath.replaceAll("/", ".");
        }
        // com.fzk.Hello.class
        String substr = absolutePath.substring(absolutePath.lastIndexOf(packageName));
        // 取出.class后缀 得到全类名 com.fzk.Hello
        return substr.substring(0, substr.lastIndexOf(".class"));
    }

    public static boolean isClassFile(Path path) {
        // 注意不能用path.endWith("lastPath")方法, 这个不是判断文件后缀的
        return path.getFileName().toString().endsWith(".class");
    }

    public static String toUnixPath(String path) {
        return path.replaceAll("\\\\", "/");
    }

    public static boolean isImage(Path path) {
        if (Files.isRegularFile(path)) {
            int i = path.toString().lastIndexOf(".");
            if (i == -1) return false;
            String suffix = path.toString().substring(i + 1);
            return imageSuffixMap.containsKey(suffix);
        }
        return false;
    }

    // 向前端展示文件大小信息, 如1.1GB, 1.2MB, 365KB
    public static String fileSizeInfo(long fileSize) {
        long K = 1 << 10;
        long M = 1 << 20;
        long G = 1 << 30;

        if (fileSize < K) {
            return fileSize + "B";
        } else if (fileSize < M) {
            return fileSize / K + "KB";
        } else if (fileSize < G) {
            long nk = (fileSize & (M - 1)) / K;
            if (nk > 999) nk = 999;
            nk=nk/100;
            return String.format("%d.%01dMB", fileSize / M, nk);
        } else {
            long nm = (fileSize & (G - 1)) / M;
            if (nm > 999) nm = 999;
            nm=nm/100;
            return String.format("%d.%01dGB", fileSize / G, nm);
        }
    }
}
